package com.peking.donations.plugins.oauth2.client.wechart;

import com.google.common.collect.Maps;
import com.peking.donations.base.security.LoggedUser;
import com.peking.donations.model.DonationUser;
import com.peking.donations.service.UserService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * @author Vigor Yuan
 */
public class WechartUserInfoTokenService implements ResourceServerTokenServices {
    protected final Log logger = LogFactory.getLog(getClass());

    private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username", "userid", "user_id", "login",
            "id", "name", "openid" };

    private final String userInfoEndpointUrl;

    private final String clientId;

    private OAuth2RestOperations restTemplate;

    @Autowired
    UserDetailsService userDetailsService;

    @Autowired
    UserService userService;

    String profile;

    private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;

    private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();

    public WechartUserInfoTokenService(String userInfoEndpointUrl, String clientId) {
        this.userInfoEndpointUrl = userInfoEndpointUrl;
        this.clientId = clientId;
    }

    public void setTokenType(String tokenType) {
        this.tokenType = tokenType;
    }

    public void setRestTemplate(OAuth2RestOperations restTemplate) {
        this.restTemplate = restTemplate;
    }

    public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {
        this.authoritiesExtractor = authoritiesExtractor;
    }

    @Override
    public OAuth2Authentication loadAuthentication(String accessToken)
            throws AuthenticationException, InvalidTokenException {
        Map<String, Object> map = Maps.newHashMap();
        if("prod".equals(profile)){
            map.put("openid", getOpenId(accessToken));
            map.put("nickname", "");
            map.put("headimgurl", "");
        }else{
            map=getMap(userInfoEndpointUrl,accessToken);
        }

        return extractAuthentication(map);
    }

    private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
        Object principal = getPrincipal(map);
        List<GrantedAuthority> authorities = this.authoritiesExtractor.extractAuthorities(map);
        OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null, null, null, null, null);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A",
                authorities);
        LoggedUser user = (LoggedUser) userDetailsService.loadUserByUsername(map.get("openid").toString());
        if (user == null) {
            user = LoggedUser.anonymous(map.get("openid").toString(), map.get("nickname").toString(),
                    map.get("headimgurl").toString());
            DonationUser donationUser = new DonationUser();
            donationUser.setWeixinOpenId(user.getOpenId());
            donationUser.setWeixinNickname(user.getNickName());
            donationUser.setWeixinHeadImg(user.getHeadImg());
            donationUser.setType(-1);
            donationUser = userService.saveUser(donationUser);
            user.setUserId(donationUser.getId());

        }
        token.setDetails(user);
        return new OAuth2Authentication(request, token);
    }

    private Object getPrincipal(Map<String, Object> map) {
        for (String key : PRINCIPAL_KEYS) {
            if (map.containsKey(key)) {
                return map.get(key);
            }
        }
        return "unknown";
    }

    @Override
    public OAuth2AccessToken readAccessToken(String accessToken) {
        throw new UnsupportedOperationException("Not supported: read access token");
    }

    @SuppressWarnings({ "unchecked" })
    private Map<String, Object> getMap(String path, String accessToken) {
        this.logger.info("Getting user info from: " + path);
        try {
            OAuth2RestOperations restTemplate = this.restTemplate;
            if (restTemplate == null) {
                BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
                resource.setClientId(this.clientId);
                restTemplate = new OAuth2RestTemplate(resource);
            }
            OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext().getAccessToken();
            if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
                DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(accessToken);
                token.setTokenType(this.tokenType);
                restTemplate.getOAuth2ClientContext().setAccessToken(token);
            }
            path = this.userInfoEndpointUrl + "?access_token=" + accessToken + "&openid="
                    + existingToken.getAdditionalInformation().get("openid") + "&lang=zh_CN";
            Map body = restTemplate.getForEntity(path, Map.class).getBody();
            logger.debug("response userinfo " + body.toString());
            return body;
        } catch (Exception ex) {
            this.logger.info("Could not fetch user details: " + ex.getClass() + ", " + ex.getMessage(), ex);
            return Collections.<String, Object> singletonMap("error", "Could not fetch user details");
        }
    }

    private String getOpenId(String accessToken) {
        OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext().getAccessToken();
        return (String) existingToken.getAdditionalInformation().get("openid");
    }

    public String getProfile() {
        return profile;
    }

    public void setProfile(String profile) {
        this.profile = profile;
    }
}
